require 'spec_helper'

describe GitReflow::GitServer::PullRequest do
  let(:pr_response)     { Fixture.new('pull_requests/external_pull_request.json').to_json_hashie }
  let(:pr)              { MockPullRequest.new(pr_response) }
  let(:github)          { stub_github_with({ user: 'reenhanced', repo: 'repo', pull: pr_response }) }
  let!(:github_api)     { github.connection }
  let(:git_server)      { GitReflow::GitServer::GitHub.new {} }
  let(:user)            { 'reenhanced' }
  let(:password)        { 'shazam' }
  let(:enterprise_site) { 'https://github.reenhanced.com' }
  let(:enterprise_api)  { 'https://github.reenhanced.com' }

  describe ".minimum_approvals" do
    subject { MockPullRequest.minimum_approvals }
    before  { allow(GitReflow::Config).to receive(:get).with('constants.minimumApprovals').and_return('2') }
    it      { should eql '2' }
  end

  describe ".approval_regex" do
    subject { MockPullRequest.approval_regex }
    it      { should eql MockPullRequest::DEFAULT_APPROVAL_REGEX }

    context "with custom approval regex set" do
      before { allow(GitReflow::Config).to receive(:get).with('constants.approvalRegex').and_return('/dude/') }
      it     { should eql Regexp.new('/dude/') }
    end
  end

  %w{commit_author comments last_comment reviewers approvals}.each do |method_name|
    describe "##{method_name}" do
      specify { expect{ pr.send(method_name.to_sym) }.to raise_error("MockPullRequest##{method_name} method must be implemented") }
    end
  end

  describe "#good_to_merge?(options)" do
    subject  { pr.good_to_merge? }

    context "with no status" do
      context "and approved" do
        before { allow(pr).to receive(:approved?).and_return(true) }
        it     { should be_truthy }
      end

      context "but not approved" do
        before { allow(pr).to receive(:approved?).and_return(false) }
        it     { should be_falsy }
      end
    end

    context "with build status" do
      context "of 'success'" do
        before  { allow(pr).to receive(:build_status).and_return('success') }

        context "and approved" do
          before { allow(pr).to receive(:approved?).and_return(true) }
          it     { should be_truthy }
        end

        context "but not approved" do
          before { allow(pr).to receive(:approved?).and_return(false) }
          it     { should be_falsy }
        end
      end

      context "NOT of 'success'" do
        before { allow(pr).to receive(:build_status).and_return('failure') }

        context "and approved" do
          before { allow(pr).to receive(:approved?).and_return(true) }
          it     { should be_falsy }
        end

        context "and not approved" do
          before { allow(pr).to receive(:approved?).and_return(false) }
          it     { should be_falsy }
        end
      end
    end

    context "force merge?" do
      subject { pr.good_to_merge?(force: true) }

      context "with no successful build" do
        before { allow(pr).to receive(:build_status).and_return(nil) }
        it     { should be_truthy }
      end

      context "with no approval" do
        before { allow(pr).to receive(:approved?).and_return(false) }
        it     { should be_truthy }
      end

      context "with neither build success or approval" do
        before do
          allow(pr).to receive(:build_status).and_return(nil)
          allow(pr).to receive(:approved?).and_return(false)
        end
        it     { should be_truthy }
      end
    end
  end

  describe "#approved?" do
    subject { pr.approved? }

    context "no approvals required" do
      before do
        allow(MockPullRequest).to receive(:minimum_approvals).and_return('0')
        allow(pr).to receive(:has_comments?).and_return(false)
        allow(pr).to receive(:approvals).and_return([])
      end
      it { should be_truthy }
    end

    context "all commenters must approve" do
      before { allow(MockPullRequest).to receive(:minimum_approvals).and_return(nil) }

      context "and there are comments" do
        before { allow(pr).to receive(:comments).and_return(['Cool.', 'This is nice.']) }
        context "and there are no reviewers pending a resposne" do
          before { allow(pr).to receive(:reviewers_pending_response).and_return([]) }
          it     { should be_truthy }
        end
        context "but there are reviewers pending a resposne" do
          before { allow(pr).to receive(:reviewers_pending_response).and_return(['octocat']) }
          it     { should be_falsy }
        end
      end

      context "and there are no comments" do
        before { allow(pr).to receive(:comments).and_return([]) }
        context "and there are approvals" do
          before { allow(pr).to receive(:approvals).and_return(['Sally']) }
          context "and there are no reviewers pending a resposne" do
            before { allow(pr).to receive(:reviewers_pending_response).and_return([]) }
            it     { should be_truthy }
          end
          context "but there are reviewers pending a resposne" do
            before { allow(pr).to receive(:reviewers_pending_response).and_return(['octocat']) }
            it     { should be_falsy }
          end
        end
        context "and there are no approvals" do
          before { allow(pr).to receive(:approvals).and_return([]) }
          it     { should be_falsy }
        end
      end
    end

    context "custom minimum approval set" do
      before { allow(MockPullRequest).to receive(:minimum_approvals).and_return('2') }

      context "with comments" do
        before { allow(pr).to receive(:comments).and_return(['Sally']) }
        context "and approvals is greater than minimum" do
          before { allow(pr).to receive(:approvals).and_return(['Sally', 'Joey', 'Randy']) }
          it     { should be_truthy }
        end

        context "and approvals is equal to minimum" do
          before { allow(pr).to receive(:approvals).and_return(['Sally', 'Joey']) }
          it     { should be_truthy }
        end

        context "but approvals is less than minimum" do
          before { allow(pr).to receive(:approvals).and_return(['Sally']) }
          it     { should be_falsy }
        end
      end

      context "without_comments" do
        before { allow(pr).to receive(:comments).and_return([]) }
        context "and approvals is greater than minimum" do
          before { allow(pr).to receive(:approvals).and_return(['Sally', 'Joey', 'Randy']) }
          it     { should be_truthy }
        end

        context "and approvals is equal to minimum" do
          before { allow(pr).to receive(:approvals).and_return(['Sally', 'Joey']) }
          it     { should be_truthy }
        end

        context "but approvals is less than minimum" do
          before { allow(pr).to receive(:approvals).and_return(['Sally']) }
          it     { should be_falsy }
        end
      end

    end

  end

  describe "#rejection_message" do
    subject { pr.rejection_message }

    context "and the build is not successful" do
      before do
        allow(pr).to receive(:build_status).and_return('failure')
        allow(pr).to receive(:build).and_return(MockPullRequest::Build.new(state: 'failure', description: 'no dice', url: 'https://example.com'))
        allow(pr).to receive(:reviewers).and_return([])
        allow(pr).to receive(:approvals).and_return([])
      end

      it { is_expected.to eql("no dice: https://example.com") }

      context "but it is blank" do
        before { allow(pr).to receive(:build_status).and_return(nil) }
        it     { is_expected.to_not eql("no dice: https://example.com") }
      end
    end

    context "and the build is successful" do
      context "but approval minimums haven't been reached" do
        before do
          allow(pr).to receive(:approval_minimums_reached?).and_return(false)
          allow(MockPullRequest).to receive(:minimum_approvals).and_return('3')
        end
        it { is_expected.to eql("You need approval from at least 3 users!") }
      end

      context "and approval minimums have been reached" do
        before { allow(pr).to receive(:approval_minimums_reached?).and_return(true) }

        context "but all comments haven't been addressed" do
          before  do
            allow(pr).to receive(:all_comments_addressed?).and_return(false)
            allow(pr).to receive(:last_comment).and_return("nope")
          end
          it { is_expected.to eql("The last comment is holding up approval:\nnope") }
        end

        context "and all comments have been addressed" do
          before { allow(pr).to receive(:all_comments_addressed?).and_return(true) }

          context "but there are still pending reviews" do
            before { allow(pr).to receive(:reviewers_pending_response).and_return(['sally', 'bog']) }
            it     { is_expected.to eql("You still need a LGTM from: sally, bog") }
          end

          context "and there are no pending reviews" do
            before { allow(pr).to receive(:reviewers_pending_response).and_return([]) }
            it     { is_expected.to eql("Your code has not been reviewed yet.") }
          end
        end
      end
    end
  end

  describe "#approval_minimums_reached?" do
    subject { pr.approval_minimums_reached? }

    before { allow(pr).to receive(:approvals).and_return(['sally', 'bog']) }

    context "minimum approvals not set" do
      before { allow(MockPullRequest).to receive(:minimum_approvals).and_return('') }
      it     { is_expected.to eql true }
    end

    context "minimum approvals not met" do
      before { allow(MockPullRequest).to receive(:minimum_approvals).and_return('3') }
      it     { is_expected.to eql false }
    end

    context "minimum approvals met" do
      before { allow(MockPullRequest).to receive(:minimum_approvals).and_return('2') }
      it     { is_expected.to eql true }
    end
  end

  describe "#all_comments_addressed?" do
    subject { pr.all_comments_addressed? }
    before  { allow(pr).to receive(:approvals).and_return(['sally', 'bog']) }

    context "minimum approvals not set" do
      before  do
        allow(MockPullRequest).to receive(:minimum_approvals).and_return('')
        allow(pr).to receive(:last_comment).and_return('nope')
      end
      it { is_expected.to eql true }
    end

    context "minimum approvals set" do
      before { allow(MockPullRequest).to receive(:minimum_approvals).and_return('2') }
      context "last comment approved" do
        before { allow(pr).to receive(:last_comment).and_return('lgtm') }
        it     { is_expected.to eql true }
      end

      context "last comment not approved" do
        before { allow(pr).to receive(:last_comment).and_return('nope') }
        it     { is_expected.to eql false }
      end
    end
  end

  describe "#display_pull_request_summary" do
    subject { pr.display_pull_request_summary }

    before do
      allow(pr).to receive(:reviewers).and_return([])
      allow(pr).to receive(:approvals).and_return([])
      allow(pr).to receive(:reviewers_pending_response).and_return([])
    end

    it "displays relevant information about the pull request" do
      expect{ subject }.to have_output("branches: #{pr.feature_branch_name} -> #{pr.base_branch_name}")
      expect{ subject }.to have_output("number: #{pr.number}")
      expect{ subject }.to have_output("url: #{pr.html_url}")
      expect{ subject }.to have_said("No one has reviewed your pull request.\n", :notice)
    end

    context "with build status" do
      let(:build) { MockPullRequest::Build.new(state: "failure", description: "no dice", url: "https://example.com") }
      before      { allow(pr).to receive(:build).and_return(build) }
      specify     { expect{ subject }.to have_said("Your build status is not successful: #{build.url}.\n", :notice) }
      context "and build status is 'success'" do
        before  { allow(build).to receive(:state).and_return('success') }
        specify { expect{ subject }.to_not have_output("Your build status is not successful") }
      end
    end

    context "with reviewers" do
      before do
        allow(pr).to receive(:reviewers).and_return(['tito', 'ringo'])
        allow(pr).to receive(:last_comment).and_return('nope')
      end

      specify { expect{ subject }.to have_output("reviewed by: #{"tito".colorize(:red)}, #{"ringo".colorize(:red)}") }
      specify { expect{ subject }.to have_output("Last comment: nope") }

      context "and approvals" do
        before  { allow(pr).to receive(:approvals).and_return(['tito']) }
        specify { expect{ subject }.to have_output  'tito'.colorize(:green) }
        specify { expect{ subject }.to have_output  'ringo'.colorize(:red) }
      end

      context "and pending approvals" do
        before  { allow(pr).to receive(:reviewers_pending_response).and_return(['tito', 'ringo']) }
        specify { expect{ subject }.to_not have_output "You still need a LGTM from: tito, ringo" }
      end

      context "and no pending approvals" do
        before  { allow(pr).to receive(:reviewers_pending_response).and_return([]) }
        specify { expect{ subject }.to_not have_output "You still need a LGTM from" }
      end
    end
  end

  context "#merge!" do
    let(:commit_message_for_merge) { "This changes everything!" }

    let(:inputs) do
      {
        base:    "base_branch",
        title:   "title",
        message: "message"
      }
    end

    let(:lgtm_comment_authors) { ["simonzhu24", "reenhanced"] }
    let(:merge_response)       { { message: "Failure_Message" } }

    subject { pr.merge! inputs }

    before do
      allow(GitReflow).to receive(:append_to_merge_commit_message)
      allow(pr).to receive(:commit_message_for_merge).and_return(commit_message_for_merge)
    end

    context "and can deliver" do
      before { allow(pr).to receive(:deliver?).and_return(true) }

      specify { expect{ subject }.to have_said "Merging pull request ##{pr.number}: '#{pr.title}', from '#{pr.feature_branch_name}' into '#{pr.base_branch_name}'", :notice }

      it "updates both feature and destination branch and squash-merges feature into base branch" do
        expect(GitReflow).to receive(:update_current_branch)
        expect(GitReflow).to receive(:fetch_destination).with(pr.base_branch_name)
        expect(GitReflow).to receive(:append_to_merge_commit_message).with(pr.commit_message_for_merge)
        expect { subject }.to have_run_commands_in_order [
          "git checkout #{pr.base_branch_name}",
          "git pull origin #{pr.base_branch_name}",
          "git merge --squash #{pr.feature_branch_name}"
        ]
      end

      context "and successfully commits merge" do
        specify { expect{ subject }.to have_said "Pull request ##{pr.number} successfully merged.", :success }

        context "and cleaning up feature branch" do
          before  { allow(pr).to receive(:cleanup_feature_branch?).and_return(true) }
          specify { expect{ subject }.to have_said "Nice job buddy." }
          it "pushes the base branch and removes the feature branch locally and remotely" do
            expect { subject }.to have_run_commands_in_order [
              "git push origin #{pr.base_branch_name}",
              "git push origin :#{pr.feature_branch_name}",
              "git branch -D #{pr.feature_branch_name}"
            ]
          end
        end

        context "but not cleaning up feature branch" do
          before  { allow(pr).to receive(:cleanup_feature_branch?).and_return(false) }
          specify { expect{ subject }.to_not have_run_command "git push origin #{pr.base_branch_name}" }
          specify { expect{ subject }.to_not have_run_command "git push origin :#{pr.feature_branch_name}" }
          specify { expect{ subject }.to_not have_run_command "git branch -D #{pr.feature_branch_name}" }
          specify { expect{ subject }.to have_said "Cleanup halted.  Local changes were not pushed to remote repo.", :deliver_halted }
          specify { expect{ subject }.to have_said "To reset and go back to your branch run \`git reset --hard origin/#{pr.base_branch_name} && git checkout #{pr.feature_branch_name}\`" }
        end

        context "and NOT squash merging" do
          let(:inputs) do
            {
              base:         "base_branch",
              title:        "title",
              message:      "message",
              merge_method: "merge"
            }
          end

          specify { expect{ subject }.to have_run_command "git merge #{pr.feature_branch_name}" }
          specify { expect{ subject }.to_not have_run_command "git merge --squash #{pr.feature_branch_name}" }
        end
      end

      context "but has an issue commiting the merge" do
        before do
          allow(GitReflow).to receive(:run_command_with_label)
          allow(GitReflow).to receive(:run_command_with_label).with('git commit', with_system: true).and_return(false)
        end
        specify { expect{ subject }.to have_said "There were problems commiting your feature... please check the errors above and try again.", :error }
      end
    end

    context "but cannot deliver" do
      before  { allow(pr).to receive(:deliver?).and_return(false) }
      specify { expect{ subject }.to have_said "Merge aborted", :deliver_halted }
    end
  end

  context "#commit_message_for_merge" do
    subject { pr.commit_message_for_merge }

    let(:lgtm_comment_authors) { ["simonzhu24", "reenhanced"] }
    let(:output)               { lgtm_comment_authors.join(', @') }
    let(:first_commit_message) { "Awesome commit here." }

    before do
      allow(GitReflow).to receive(:get_first_commit_message).and_return(first_commit_message)
      allow(pr).to receive(:approvals).and_return []
    end

    specify { expect(subject).to include "\nMerges ##{pr.number}\n" }
    specify { expect(subject).to_not include "\nLGTM given by: " }

    context "with description" do
      before  { allow(pr).to receive(:description).and_return("Bingo.") }
      specify { expect(subject).to include "Bingo." }
      specify { expect(subject).to_not include first_commit_message }
    end

    context "with no description" do
      before do
        allow(pr).to receive(:description).and_return('')
      end
      specify { expect(subject).to include first_commit_message }
    end

    context "with approvals" do
      before  { allow(pr).to receive(:approvals).and_return(['sally', 'joey']) }
      specify { expect(subject).to include "\nLGTM given by: @sally, @joey\n" }
    end

    context "with custom merge commit message template" do
      before { allow(GitReflow).to receive(:merge_commit_template).and_return("Super cool changes") }
      specify { expect(subject).to include "Super cool changes" }
      specify { expect(subject).to_not include "\nMerges ##{pr.number}\n" }
    end
  end

  context "#cleanup_feature_branch?" do
    subject { pr.cleanup_feature_branch? }

    context "and always cleanup config is set to 'true'" do
      before { allow(GitReflow::Config).to receive(:get).with('reflow.always-cleanup').and_return('true') }
      it     { should be_truthy }
    end

    context "and always cleanup config is not set" do
      before do
        allow(GitReflow::Config).to receive(:get).with('reflow.always-cleanup').and_return('false')
      end

      context "and always cleanup local config is set" do
      end

      context "and always cleanup remote config is set" do
      end

      context "and user chooses to cleanup local only" do
        before do
          expect(pr).to receive(:ask).with('Would you like to cleanup your local feature branch? ').and_return('yes')
          allow(pr).to receive(:ask).with('Would you like to cleanup your remote feature branch? ').and_return('no')
        end
        it { should be_truthy }
      end

      context "and user chooses to cleanup remote only" do
        before do
          expect(pr).to receive(:ask).with('Would you like to cleanup your local feature branch? ').and_return('no')
          expect(pr).to receive(:ask).with('Would you like to cleanup your remote feature branch? ').and_return('yes')
        end
        it     { should be_truthy }
      end

      context "and user chooses to cleanup local and remote" do
        before do
          allow(pr).to receive(:ask).with('Would you like to cleanup your local feature branch? ').and_return('yes')
          allow(pr).to receive(:ask).with('Would you like to cleanup your remote feature branch? ').and_return('yes')
        end
        it { should be_truthy }
      end

      context "and user chooses to cleanup neither local nor remote" do
        before do
          expect(pr).to receive(:ask).with('Would you like to cleanup your local feature branch? ').and_return('no')
          allow(pr).to receive(:ask).with('Would you like to cleanup your remote feature branch? ').and_return('no')
        end
        it     { should be_falsey }
      end
    end
  end

  context "#deliver?" do
    subject { pr.deliver? }

    context "and always deliver config is set to 'true'" do
      before { allow(GitReflow::Config).to receive(:get).with('reflow.always-deliver').and_return('true') }
      it     { should be_truthy }
    end

    context "and always deliver config is not set" do
      before do
        allow(GitReflow::Config).to receive(:get).with('reflow.always-deliver').and_return('false')
      end

      context "and user chooses to deliver" do
        before { expect(pr).to receive(:ask).with('This is the current status of your Pull Request. Are you sure you want to deliver? ').and_return('yes') }
        it     { should be_truthy }
      end

      context "and user choose not to cleanup" do
        before { expect(pr).to receive(:ask).with('This is the current status of your Pull Request. Are you sure you want to deliver? ').and_return('no') }
        it     { should be_falsy }
      end
    end
  end
end
